package agents;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import negotiator.Agent;
import negotiator.Bid;
import negotiator.BidIterator;
import negotiator.actions.Accept;
import negotiator.actions.Action;
import negotiator.actions.EndNegotiation;
import negotiator.actions.Offer;

/**
 * Agent based on an Adaptive Bilateral Negotiating Strategy over Multiple Items
 * @author Oliver Trinnes, Thomas Nowicki, Felix Remmel, Ruben Schwarzwald
 */
public class AgentFeruthol extends Agent
{
  /**
   * Controlling variable determining the way the acceptance threshold
   * approaches the discounted maximum utility
   */
  private static final double ALPHA = 1;
  /**
   * Controlling variable determining the way the value of the exploitation
   * point lambda changes with respect to the discounting factor
   */
  private static final double BETA = 10;
  /**
   * Controlling variable determining the way the value of the exploitation
   * point lambda changes with respect to the partners concessive degree sigma
   */
  private static final double GAMMA = 1;
  /**
   * Probability with which a negotiation outcome is randomly chosen from the
   * candidates C
   */
  private static final double EPSILON = 0.8;
  /**
   * Initial value of lambda, which describes the point where the agent stops
   * exploiting the negotiating partner
   */
  private static final double LAMBDA0 = 0.2;
  /**
   * Disounting factor reflecting the decreasing speed of the relative
   * importance of the negotiation outcomes that are saved in the Frequency
   * Map
   */
  public static final double ETA = 0.9;
  /**
   * The weighting factor adjusting the relative effect of the concessive degree
   * of the partner on the non exploitation point lambda
   */
  private static final double WEIGHT = 0.001;
  /**
   * Determining how many of the last history bids are considered recent
   */
  private static final int recentHistorySize = 10;
  /**
   * Stack of all the incoming bids
   */
  private Stack<Bid> history;
  /**
   * All possible bids
   */
  private ArrayList<Bid> allPossibleBids;
  /**
   * Point when the agents stops expoiting his partner
   */
  private double lambda;
  /**
   * Flag for the next bid component
   */
  boolean proposeWBestNextTime;
  /**
   * Stores the frequencies of every issue value pair proposed by the partner
   */
  private FrequencyMap frequencyMap;
  private double lowestThreshold;

  /**
   * Called when another session is started
   */
  @Override
  public void init()
  {
    history = new Stack<Bid>();
    BidIterator iterator = new BidIterator(utilitySpace.getDomain());
    allPossibleBids = new ArrayList<Bid>();
    while(iterator.hasNext())
      allPossibleBids.add(iterator.next());
    proposeWBestNextTime = false;
    lambda = 0;
    frequencyMap = new FrequencyMap(utilitySpace);
    lowestThreshold = 1;
  }

  /*
   * CHOOSE NEXT ACTION
   */
  @Override
  public Action chooseAction()
  {
    Action action = null;
    try
    {
      sleep(0.001); // To keep an overview of the process
      double threshold = getAcceptanceThreshold();
      if(lowestThreshold > threshold)
        lowestThreshold = threshold;
      System.out.println("Threshold = "+threshold);
      Bid outcome = getNextBid(threshold);
      double ru = utilitySpace.getReservationValueWithDiscount(timeline);

      boolean acceptanceCondition = isAcceptable(threshold, outcome);
      boolean terminationCondition = isTerminatable(threshold, outcome, ru);

      if (history.isEmpty())
      {
        if (!terminationCondition)
          return new Offer(getAgentID(), outcome);
        else
          return new EndNegotiation(this.getAgentID());
      }
      else
      {
        if (acceptanceCondition && !terminationCondition)
          return new Accept(getAgentID());
        else if (!acceptanceCondition && terminationCondition)
          return new EndNegotiation(this.getAgentID());
        else if (acceptanceCondition && terminationCondition)
        {
          Bid lastBid = history.peek();
          if (getUtility(lastBid) > ru)
            return new Accept(getAgentID());
          else
            return new EndNegotiation(this.getAgentID());
        }
        else
          return (new Offer(getAgentID(), outcome));
      }

    }
    catch (Exception ex)
    {
      Logger.getLogger(AgentFeruthol.class.getName()).log(Level.SEVERE, null, ex);
    }
    return action;
  }

  /*
   * Acceptance Threshold Component
   */
  private double getAcceptanceThreshold() throws Exception
  {
    double t = timeline.getTime();
    System.out.println("Time: "+t);
    updateLambda(t);
    System.out.println("Lambda = "+lambda);
    Bid maxUtilityBid = utilitySpace.getMaxUtilityBid();
    double uMax = utilitySpace.getUtility(maxUtilityBid);
    if (t < lambda)
    {
      double utilityWithDiscountLambda =
              utilitySpace.getUtilityWithDiscount(maxUtilityBid, 1 - lambda);
      return uMax - (uMax - utilityWithDiscountLambda) * Math.pow(t
              / lambda, ALPHA);
    }
    return utilitySpace.getUtilityWithDiscount(maxUtilityBid, 1 - t);
  }

  /*
   * Updates the non-exploitation point lambda
   */
  private void updateLambda(double t)
  {
    if (t == 0 || history.isEmpty())
    {
      double discountFactor = utilitySpace.getDiscountFactor();
      lambda = LAMBDA0 + (1 - LAMBDA0)
              * Math.pow(discountFactor, BETA);
    }
    else if (t > 0 && t <= 1.)
    {
      List<Bid> differentRecentOfferedUtils = new ArrayList<Bid>();
      for (int i = 0; i < recentHistorySize; i++)
      {
        if (history.size() - 1 - i < 0)
          break;
        Bid bid = history.get(history.size() - 1 - i);

        /**
         * The following code makes sure that it is determined correctly if the
         * list of bids contains a certain bid by using the customized equals
         * method
         */
        boolean contains = false;
        for (Bid recentBid : differentRecentOfferedUtils)
          if (recentBid.equals(bid))
          {
            contains = true;
            break;
          }
        if (!contains)
          differentRecentOfferedUtils.add(bid);
      }
      double sigma = (double) differentRecentOfferedUtils.size()
              / recentHistorySize;
      lambda += WEIGHT * (1 - lambda) * Math.pow(sigma, t * GAMMA);
    }
  }

  /*
   * Next Bid Component
   */
  private Bid getNextBid(double threshold) throws Exception
  {
    Bid maxUtilityBid = null;
    try
    {
      maxUtilityBid = utilitySpace.getMaxUtilityBid();
    }
    catch (Exception ex)
    {
      Logger.getLogger(AgentFeruthol.class.getName()).log(Level.SEVERE, null, ex);
    }

    if (history.isEmpty())
      return maxUtilityBid;

    /**
     * Checks if the acceptance component changed the flag to propose
     * the best bid of the history
     */
    if (proposeWBestNextTime)
    {
      proposeWBestNextTime = false;
      return getBestNegotiationOutcomeOfHistory();
    }
    // Set candidates to all bids with utility between threshold and 1
    List<Bid> bidsInRange = new ArrayList<Bid>();
    for(int i = 0; i<allPossibleBids.size(); i++)
    {
      Bid bid = allPossibleBids.get(i);
      double utility = 0;
      try
      {
        utility = utilitySpace.getUtility(bid);
      }
      catch (Exception ex)
      {
        Logger.getLogger(AgentFeruthol.class.getName()).log(Level.SEVERE, null, ex);
      }
      if(utility>threshold)
        bidsInRange.add(bid);
    }
    
    System.out.println("Bids in Range: "+bidsInRange.size());
    
    if(bidsInRange.isEmpty())
      return maxUtilityBid;
    
    /*
     * Create a new array of canditates and sort them by the frequency they occurred
     */
    Bid[] candidates = new Bid[bidsInRange.size()];
    double[] frequencies = new double[bidsInRange.size()];
    for (int i = 0; i < bidsInRange.size(); i++)
    {
      Bid candidate = bidsInRange.get(i);
      candidates[i] = candidate;
      frequencies[i] = frequencyMap.getFrequency(candidate);
    }
    Sort.bubbleSort(candidates, frequencies);
    
    /**
     * Use epsilon greedy to determine if the bid with the highest frequency
     * is chosen or a random candidate bid
     */
    double random = Math.random();
    if (random < EPSILON)
      return candidates[0];
    return candidates[(int) (Math.random() * candidates.length)];
  }

  /*
   * Acceptance Condition Comnponent
   */
  private boolean isAcceptable(double threshold, Bid outcome) throws Exception
  {
    double utilityOutcome = utilitySpace.getUtility(outcome);
    Bid wBest = getBestNegotiationOutcomeOfHistory();
    double utilityWBest = utilitySpace.getUtility(wBest);
    if(history.isEmpty())
      return false;
    Bid w1 = history.peek();
    double utilityW1 = utilitySpace.getUtility(w1);
    if (utilityW1 > threshold || utilityW1 > utilityOutcome)
      return true;
    else if (utilityWBest > threshold || utilityWBest > utilityOutcome)
    {
      proposeWBestNextTime = true;
      return false;
    }
    else if(timeline.getTime() > lambda && utilityW1 > lowestThreshold)
      return true;
    return false;
  }

  private Bid getBestNegotiationOutcomeOfHistory() throws Exception
  {
    if (history.isEmpty())
      try
      {
        return utilitySpace.getMaxUtilityBid();
      }
      catch (Exception ex)
      {
        Logger.getLogger(AgentFeruthol.class.getName()).log(Level.SEVERE, null, ex);
      }
    Bid maxBid = history.peek();
    for (Bid bid : history)
      if (utilitySpace.getUtility(bid) > utilitySpace.getUtility(maxBid))
        maxBid = bid;
    return maxBid;
  }

  /*
   * Termination Condition Component
   */
  private boolean isTerminatable(double threshold, Bid outcome, double ru) throws Exception
  {
    if (ru > threshold || ru > utilitySpace.getUtility(outcome))
      return true;
    return false;
  }

  public static String getVersion()
  {
    return "1.0";
  }

  @Override
  public String getName()
  {
    return "Agent Feruthol";
  }

  /**
   * The offer of the opponent is added to the history and
   * the frequencies of the issue value pairs are updated
   * @param opponentAction 
   */
  @Override
  public void ReceiveMessage(Action opponentAction)
  {
    if (opponentAction instanceof Offer)
    {
      Bid bid = ((Offer) opponentAction).getBid();
      frequencyMap.addBid(bid);
      history.push(bid);
    }
  }
}
